Tester.php

<?php

namespace Tlf;

/**
 * Base class for test classes
 * @note main class file executes tests. Traits contain everything you use INSIDE a test
 */
class Tester {

    use Tester\Assertions;
    use Tester\Exceptions;
    use Tester\Databasing;
    use Tester\Utilities;
    use Tester\Server;
    use Tester\Other;

    protected $catchers = [];
    /**
     * Comparisons from a single test. Should be reset between tests.
     */
    protected $assertions = ['pass'=> 0, 'fail'=>0];
    protected $enabled = true;

    protected $options = [];

    /**
     * The cli class used to run the tests
     */
    public $cli = null;

    /**
     * The string name of the method being called for the current test. Like `"testSomething"`
     */
    public ?string $current_test = null;

    /**
     * @param $options usually args passed from the command line. 
     */
    public function __construct(array $options=[], $cli=null){
        $this->backward_compatability();
        if ($cli==null)$this->options = $options;
        else $this->options = &$cli->args;//$options;
        //$this->options
        if ($this->options['set_error_handler']??true){
            set_error_handler([$this,'throwError']);
        }
        $this->cli = $cli;

        if (!isset($this->options['test']))$this->options['test'] = [];
        if (!isset($this->options['class']))$this->options['class'] = [];

    }
    public function throwError($errno, $errstr, $errfile, $errline) {
        throw new \ErrorException($errstr, $errno, 0, $errfile, $errline);
    }


    /** @beta(may 4, 2022) 
     * @param $test_name the name of the test (the portion of the method name after `test`)
     */
    public function onBeforeTest(){}
    /**
     * @deprecated This method will be removed in v0.4
     */
    public function backward_compatability(){}


    /**
     * Get array of test methods names
     * @return array like `['testMethodOne', 'testMethodTwo']`
     */
    public function get_test_methods(){
        $list = [];
        foreach (get_class_methods($this) as $method){
            if ($method=='test')continue;
            if (substr($method,0,4)!='test')continue;
            $list[] = $method;
        }

        // @TODO allow multiple 'test' params to be passed. I think this worked previously, but now it's broken.
       
        //@bugfix args['test'] is supposed to be an array ... but I think I broke it at some point. So this is_string() check is to convert it to an array
        if (is_string($this->cli->args['test']))$this->cli->args['test'] = [$this->cli->args['test']];
        if (count($this->cli->args['test'])>0){
            $tests = array_flip($this->cli->args['test']);
            $list = array_filter($list,
                function($test_name) use ($tests){
                    return isset($tests[substr($test_name,4)]);
                }
            );
        }

        return $list;
    }

    /**
     * get a readable name from a test method name
     */
    public function get_test_name($method_name): string{
        $name = $method_name;
        if (substr($method_name,0,4)=='test')$name = substr($method_name,4);

        return $name;
    }

    public function run_test_method($method){
        $name = $this->get_test_name($method);
        $test = 
        [
            'method'=>$method, 
            'error'=>null, 
            'name'=>$name,
            'pass'=>false,
            'enabled'=>$this->enabled,
        ];
        $this->onBeforeTest($name);

        $this->assertions = ['pass'=>0, 'fail'=>0];
        $this->catchers = [];
        $bench_start = microtime(true);
        $ob_level = $this->startOb();
        try {
            $this->current_test = $method;
            $this->$method();
        } catch (\Throwable $t){
            $test['error'] = $t->__toString();
            echo $test['error'];
        } 
        $this->current_test = null;
        $test['enabled'] = $this->enabled;
        $test['assertions'] = $this->assertions;
        $test['output'] = $this->endOb($ob_level);
        $test['bench'] = $this->benchEnd($bench_start);

        if ($this->assertions['pass']>=1
            &&$this->assertions['fail']===0
            &&$test['error'] === null
        ){
            $test['pass'] = true;
        }
        return $test;
    }

    public function print_test_results($test){
        $status = $test['pass'] ? 'PASS' : 'FAIL';
        $symbol = $test['pass'] ? "\033[0;32m++\033[0m" : "\033[0;31m--\033[0m";
        // print_r($test);
        // exit;
        if (in_array($test['name'], $this->options['test'])){
            if ($test['enabled']!=true)$symbol = '/';
            $str = str_repeat($symbol, 15);
            echo "\n$str ".$test['name']."[start] $str\n";
            echo $test['output'];


            if (($c=count($this->catchers))>0){
                echo "\n\n  EXCEPTION FAIL:{$c} exceptions were not handled.";
            }

            $class = get_class($this);
            echo "\n$str ".$test['name']."[end] ($class) $str";

            return;
        }

        if ($test['enabled']!=true)$symbol = '//';
        // $assertions =
            // '+'.$test['assertions']['pass']
            // .', -'.$test['assertions']['fail'];
            // ;

        $bench = '';
        if ($test['bench']['diff']>$this->options['bench.threshold']){
            $ms = $test['bench']['diff'] * 1000;
            $ms = number_format($ms,4);
            $bench=' '.$ms.'ms';
        }

        $assertions = '';
        if ($test['pass']){
            $assertions = "  (+".$test['assertions']['pass'].')';
        } else {
            $assertions = "  (+".$test['assertions']['pass'].", -".$test['assertions']['fail'].')';
        }


        echo "\n  $symbol ".$test['name']. $bench . $assertions; //." ($assertions)";

    }
    /**
     * Run tests
     *
     * @param $methods an array of method names to run as tests or NULL to run all methods beginning with 'test'
     */
    public function run(){


        $class = explode('\\',get_class($this));
        $name = array_pop($class);
        echo "\n". array_pop($class).'\\'.$name.': ';
        $methods = $this->get_test_methods();
        $results = [
            'class'=>get_class($this),
            'tests_run'=>0,
            'pass'=>0,
            'fail'=>0,
            'disabled'=>0,
            'assert_pass'=>0,
            'assert_fail'=>0,
            'failed_tests'=>[],
        ];
        $tests = [];

        if (count($methods)==0)return $results;
        $this->prepare();
        
        foreach ($methods as $method){
            $this->inverted = false;
            $this->enabled = true;
            
            try {
                $this->will_run_test($method);
            } catch (\Exception $e){
                $class = get_class($this);
                echo "  $class::will_run_test() threw exception."; // for test method '$method' on class '$class'";
                if (isset($this->cli->args['test'])&& !empty($this->cli->args['test'])){
                    echo "\nException: ".$e->getMessage();
                    echo "\nStackTrace: ". $e->getTraceAsString();
                }
                continue;
            }
            $test = $this->run_test_method($method);
            try {
                $this->did_run_test($method, $test);
            } catch (\Exception $e){
                $class = get_class($this);
                echo "  $class::did_run_test() threw exception"; // for test method '$method' on class '$class'";
                if (isset($this->cli->args['test']) && !empty($this->cli->args['test'])){
                    echo "\nException: ".$e->getMessage();
                    echo "\nStackTrace: ". $e->getTraceAsString();
                }
            }
            if ($test===false){
                continue;
            }
            $this->print_test_results($test);

            $tests[] = $test;

            $results['tests_run']++;
            if ($test['enabled']!==true){
                $results['disabled']++;
            } else if ($test['pass']===true){
                $results['pass']++;
            } else {
                $results['failed_tests'][] = $method;
                $results['fail']++;
            }

            $results['assert_pass']+=$test['assertions']['pass'];
            $results['assert_fail']+=$test['assertions']['fail'];

            // $test['enabled'] = $this->enabled;
            // print_r($test['assertions']);
            // exit;
        }

        $this->finish();
        // echo "\n  ".$results['fail'].' fail, '.$results['pass'].' pass';;

        // $results['tests'] = $tests;
        // $results = $this->assertions;
        // print_r($results);
        // exit;
//
        // print_r($this->assertions);
        // exit;
    
        return $results;
    }
    /**
     * @param $start_time a value from `microtime(true)`
     */
    public function benchEnd($start_time){
        $end = microtime(true);
        $diff = $end - $start_time;
        return [
            'start'=>$start_time,
            'end'=>$end,
            'diff'=>$diff
        ];
    }

    /**
     * @override to execute before a single test runs
     */
    public function will_run_test(string $method_name){}

    /**
     * @override to execute after a single test runs
     */
    public function did_run_test(string $method_name, array $test_result){}

}